/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2020-2020.
 * Description: decoupling ioremap_tracer into this file
 * Author: lijiahuan
 * Create: 2020-12-21
 */
#include <linux/rtos_ioremap.h>
#include <linux/rtos_sysctl.h>

#define IOREMAP_TRACE_MAXLEN 42
#define MAX_RANGES_SIZE 10

static char ioremap_trace_ranges[IOREMAP_TRACE_MAXLEN * MAX_RANGES_SIZE];
static DEFINE_MUTEX(ioremap_trace_lock);

#if defined(CONFIG_RTOS_KBOX)
#include <linux/set_kbox_region.h>

unsigned int ioremap_log_size;
int ioremap_kbox_region_id = -1;

#define MAX_UINT (~0U)
#define MAX_LOG_SIZE (MAX_UINT/1024)

#endif
int tracer_ioremap_enabled;

struct trace_region_list {
	phys_addr_t start;
	phys_addr_t size;
	struct list_head trace_region;
};
static DEFINE_RWLOCK(trace_list_lock);
static LIST_HEAD(all_trace_region);
static unsigned int trace_region_list_cnt;

static void update_ioremap_trace_ranges(void)
{
	struct trace_region_list *trace_p;
	unsigned int sum_cnt = 0;

	read_lock(&trace_list_lock);
	list_for_each_entry(trace_p, &all_trace_region, trace_region) {
		if (sum_cnt >= sizeof(ioremap_trace_ranges)) {
			pr_err("[ioremap]out of ioremap_trace_ranges size[%d]\n", MAX_RANGES_SIZE);
			break;
		}
		sum_cnt += snprintf(ioremap_trace_ranges + sum_cnt,
			sizeof(ioremap_trace_ranges) - sum_cnt,
			"0x%llx-0x%llx\n",
			(unsigned long long)trace_p->start,
			(unsigned long long)(trace_p->start + trace_p->size));
	}
	if (trace_region_list_cnt == 0)
		strcpy(ioremap_trace_ranges, "\0");

	read_unlock(&trace_list_lock);
}

static int check_region_with_trace_region(struct trace_region_list *trace_p, phys_addr_t start, phys_addr_t len)
{
	if (start == trace_p->start && len == trace_p->size) {
		pr_info("[ioremap] [0x%llx-0x%llx] exist, do not add repeatedly.\n",
			(unsigned long long)trace_p->start,
			(unsigned long long)(trace_p->start + trace_p->size));
		return 0;
	}

	pr_err("[ioremap]Error: new region [0x%llx-0x%llx] conflicts with old region [0x%llx-0x%llx].\n",
		(unsigned long long)start,
		(unsigned long long)(start + len),
		(unsigned long long)trace_p->start,
		(unsigned long long)(trace_p->start + trace_p->size));
	return -EBUSY;
}

static struct trace_region_list *search_list_trace_region(phys_addr_t start, phys_addr_t len)
{
	struct trace_region_list *trace_p;

	list_for_each_entry(trace_p, &all_trace_region, trace_region)
		if ((trace_p->start < start + len) && (start < trace_p->start + trace_p->size))
			return trace_p;

	return NULL;
}

static int add_region_without_trace_region(phys_addr_t start, phys_addr_t len)
{
	struct trace_region_list *trace_p;

	trace_p = kzalloc(sizeof(struct trace_region_list),  GFP_ATOMIC);
	if (!trace_p) {
		pr_err("[ioremap]kzalloc trace_region_list failed\n");
		return -ENOMEM;
	}

	trace_p->start = start;
	trace_p->size = len;
	list_add_tail(&trace_p->trace_region, &all_trace_region);
	trace_region_list_cnt++;
	return 0;
}

static int add_region(phys_addr_t start, phys_addr_t len)
{
	int res = 0;
	struct trace_region_list *trace_p;

	write_lock(&trace_list_lock);

	trace_p = search_list_trace_region(start, len);
	if (trace_p) {
		res = check_region_with_trace_region(trace_p, start, len);
	} else {
		if (trace_region_list_cnt < MAX_RANGES_SIZE) {
			res = add_region_without_trace_region(start, len);
		} else {
			res = -EINVAL;
			pr_err("[ioremap]Error: out of ioremap_trace_ranges size[%d]\n", MAX_RANGES_SIZE);
		}
	}

	write_unlock(&trace_list_lock);

	return res;
}

static int del_region(phys_addr_t start, phys_addr_t len)
{
	struct trace_region_list *trace_p;
	phys_addr_t start_old;
	phys_addr_t len_old;
	int ret = 0;

	write_lock(&trace_list_lock);
	trace_p = search_list_trace_region(start, len);
	if (trace_p) {
		if (trace_p->start == start && trace_p->size == len) {
			list_del(&trace_p->trace_region);
			trace_region_list_cnt--;
			kfree(trace_p);
		} else {
			start_old = trace_p->start;
			len_old = trace_p->size;
			pr_err("[ioremap]Error: delete [0x%llx-0x%llx] failed, conflicts with region [0x%llx-0x%llx].\n",
				(unsigned long long)start,
				(unsigned long long)(start + len),
				(unsigned long long)start_old,
				(unsigned long long)(start_old + len_old));
			ret = -EBUSY;
		}
	} else {
		pr_err("[ioremap]Error: not exist region [0x%llx-%llx], delete failed.\n",
				(unsigned long long)start, (unsigned long long)(start + len));
		ret = -ENXIO;
	}
	write_unlock(&trace_list_lock);

	return ret;
}

static void clear_all_region(void)
{
	struct trace_region_list *trace_p, *trace_p_tmp;

	write_lock(&trace_list_lock);
	/* traversal for safe deletion of nodes, otherwise, panic occurs */
	list_for_each_entry_safe(trace_p, trace_p_tmp, &all_trace_region, trace_region) {
		list_del(&trace_p->trace_region);
		kfree(trace_p);
	}
	trace_region_list_cnt = 0;
	write_unlock(&trace_list_lock);
}

static bool check_ioremap_trace_range(phys_addr_t paddr, size_t size)
{
	struct trace_region_list *trace_p;

	read_lock(&trace_list_lock);
	trace_p = search_list_trace_region(paddr, size);
	if (trace_region_list_cnt != 0 && !trace_p) {
		read_unlock(&trace_list_lock);
		return false;
	}

	read_unlock(&trace_list_lock);
	return true;
}

void ioremap_tracer(phys_addr_t paddr, unsigned long vaddr, size_t size)
{
#if defined(CONFIG_RTOS_KBOX)
	int old_region_id = -1;
#endif
	if (tracer_ioremap_enabled) {
		if (tracer_ioremap_enabled == LOG_RANGE_ALL_MEM &&
			!check_ioremap_trace_range(paddr, size))
			return;
#ifdef CONFIG_RTOS_KBOX
		if (ioremap_kbox_region_id >= 0)
			old_region_id = set_kbox_region(ioremap_kbox_region_id);
#endif
		pr_info("[ioremap]Process %s[%d] ioremap phys addr[0x%llx, 0x%llx], virtual addr[0x%pK, 0x%pK].\n",
			current->comm, current->pid,
			(unsigned long long)paddr,
			(unsigned long long)(paddr + size),
			(void *)vaddr,
			(void *)(vaddr + size));
#ifdef CONFIG_RTOS_KBOX
		if (old_region_id >= 0)
			restore_kbox_region(old_region_id);
#endif
	}
}

static int modify_region_without_lock(char *ptr, phys_addr_t start, phys_addr_t size)
{
	int ret;

	if (*ptr == '\0') {
		ret = add_region(start, size);
		if (ret < 0)
			return -EINVAL;
	} else if (strncmp(ptr, " delete", 7) == 0) {
		if (*(ptr + 7) != '\0')
			return -EINVAL;
		ret = del_region(start, size);
		if (ret < 0)
			return -EINVAL;
	} else {
		return -EINVAL;
	}
	update_ioremap_trace_ranges();
	return 0;
}

static int proc_ioremap_trace_range(struct ctl_table *table, int write,
					void __user *buffer,
					size_t *lenp, loff_t *ppos)
{
	int ret;
	char *str_start = ioremap_trace_ranges;
	char *str_end = NULL;
	char ioremap_trace_ranges_prev[IOREMAP_TRACE_MAXLEN * MAX_RANGES_SIZE];
	phys_addr_t tmp_ioremap_trace_start;
	phys_addr_t tmp_ioremap_trace_size;

	mutex_lock(&ioremap_trace_lock);
	if (!write) {
		ret = proc_dostring(table, write, buffer, lenp, ppos);
		mutex_unlock(&ioremap_trace_lock);
		return ret;
	}

	strcpy(ioremap_trace_ranges_prev, ioremap_trace_ranges);

	ret = proc_dostring(table, write, buffer, lenp, ppos);
	if (ret) {
		mutex_unlock(&ioremap_trace_lock);
		return ret;
	}

	/* echo "" to empty /proc/sys/kernel/ioremap_trace_range */
	if (strlen(ioremap_trace_ranges) == 0) {
		clear_all_region();
		mutex_unlock(&ioremap_trace_lock);
		return 0;
	}

	tmp_ioremap_trace_size = (phys_addr_t)memparse(str_start, &str_end);
	/*
	 * 1. ioremap range size is not pagesize aligned
	 * 2. size and start addr not separated with @
	 * 3. start addr not exist
	 * 4. ioremap range size is 0
	 */
	if ((tmp_ioremap_trace_size & PAGE_MASK) != tmp_ioremap_trace_size ||
		*str_end != '@' || strlen(str_end) <= 1 || tmp_ioremap_trace_size == 0)
		goto err;

	str_start = str_end + 1;
	tmp_ioremap_trace_start = (phys_addr_t)memparse(str_start, &str_end);
	/*
	 * 1. start addr incorrect format
	 * 2. non-numeric character after start addr
	 * 3. start addr is not pagesize aligned
	 * 4. addr + size overflow
	 */
	if (str_start == str_end ||
		(tmp_ioremap_trace_start & PAGE_MASK) != tmp_ioremap_trace_start ||
		tmp_ioremap_trace_start + tmp_ioremap_trace_size <= tmp_ioremap_trace_start)
		goto err;

	if (modify_region_without_lock(str_end, tmp_ioremap_trace_start, tmp_ioremap_trace_size) < 0)
		goto err;

	mutex_unlock(&ioremap_trace_lock);
	return 0;
err:
	strcpy(ioremap_trace_ranges, ioremap_trace_ranges_prev);
	mutex_unlock(&ioremap_trace_lock);
	return -EINVAL;
}

#if defined(CONFIG_RTOS_KBOX)
int proc_ioremap_log_size(struct ctl_table *table, int write,
					void __user *buffer,
					size_t *lenp, loff_t *ppos)
{
	int tmpid, ret;
	unsigned int pre_size, region_size;

	if (!write)
		return proc_dointvec_minmax(table, write, buffer, lenp, ppos);

	pre_size = ioremap_log_size;

	ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
	if (ret)
		return ret;

	if (ioremap_log_size == 0) {
		ioremap_log_size = pre_size;
		return -EINVAL;
	}

	if (ioremap_log_size > MAX_LOG_SIZE) {
		pr_err("proc_ioremap_log_size is too large!\n");
		ioremap_log_size = pre_size;
		return -EINVAL;
	}

	region_size = ioremap_log_size * 1024;
	tmpid = kern_kbox_register_region("ioremap", region_size, NULL);
	if (tmpid < 0) {
		ioremap_log_size = pre_size;
		pr_err("register ioremap_log_size kbox region failed with %d!\n", tmpid);
		return tmpid;
	}

	ioremap_kbox_region_id = tmpid;
	return 0;
}
#endif

static int two = 2;
static struct ctl_table rtos_ioremap_sysctls[] = {
#ifdef CONFIG_RTOS_KBOX
	{
		.procname       = "ioremap_log_size",
		.data           = &ioremap_log_size,
		.maxlen         = sizeof(unsigned int),
		.mode           = 0640,
		.proc_handler   = proc_ioremap_log_size,
	},
#endif
	{
		.procname       = "ioremap_trace_range",
		.data           = ioremap_trace_ranges,
		.maxlen         = sizeof(ioremap_trace_ranges),
		.mode           = 0640,
		.proc_handler   = proc_ioremap_trace_range,
	},
	{
		.procname       = "tracer_ioremap",
		.data           = &tracer_ioremap_enabled,
		.maxlen         = sizeof(int),
		.mode           = 0640,
		.proc_handler   = proc_dointvec_minmax,
		.extra1         = SYSCTL_ZERO,
		.extra2         = &two,
	},
	{}
};

rtos_kernel_sysctls_proc_init(rtos_ioremap, rtos_ioremap_sysctls);
